%%%-------------------------------------------------------------------
%%% @author liuwentao
%%% @doc
%%%         角色数据存取服务
%%% @end
%%% Created : 08. 7月 2021 16:35
%%%-------------------------------------------------------------------
-module(role_data).
-behaviour(gen_server).
-export([
    start_link/0                %% 启动数据存取进程
    , create/6                  %% 创建角色数据
    , create/7                  %% 创建角色数据
    , list/2                    %% 查询角色列表
    , save/2                    %% 保存数据
    , create_save/1             %% 创建保存
    , local_fetch/4             %% 获取角色数据
    , local_fetch_role_id/4     %% 获取角色数据
    , get_role_list/4           %% 获取角色列表
    , local_save/2              %% 保存角色数据
    , clean_cache/1             %% 清除缓存
    , save_simple_to_db/1       %% 更新数据
    , check_return/1            %% 检查初始化标识
]
).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-include_lib("stdlib/include/qlc.hrl").
-include("common.hrl").
-include("role.hrl").
-include("emysql.hrl").

-define(DETS_NUM, 5).   %% 启动的DETS数量，注意此值在运行中的系统不可随便更改
-define(ROLE_DATA_TAB(N), list_to_atom("role_data_" ++ integer_to_list(N))). %% 获取dets表名
-define(RID_TO_TAB(Rid), ?ROLE_DATA_TAB(Rid rem ?DETS_NUM + 1)). %% 根据Rid获取dets表名,单个dets上限是2G,所以数据量大的时候,可能会超.

-record(state, {
    next_id = 1
}
).

%% ----------------------------------------------------
%% 对外接口
%% ----------------------------------------------------

%% @doc 启动
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% @doc 创建一个新角色
-spec create(Account :: bitstring(), Platform :: bitstring(), ZoneId :: non_neg_integer(), Name :: auto | bitstring(), Sex :: non_neg_integer(), Classes :: non_neg_integer(), ConnPid :: pid()) -> {ok, #role{}} | {error, term()}.
create(Account, Platform, ZoneId, Name, Sex, Classes, ConnPid) ->
    %% 检查平台标识和区号是否合法
    case (Platform =:= env:get(platform) andalso ZoneId =:= env:get(zone_id))
        orelse lists:member({Platform, ZoneId}, env:get(combine)) of
        false -> {error, server_id_error};
        true ->
            case create_name(Name, {Account, ConnPid}) of
                {false, Reason} -> {false, Reason};
                true ->
                    NewRole = create(Account, Platform, ZoneId, Name, Sex, Classes),
                    {ok, NewRole}
            end
    end.

%% 初始化角色数据
-spec create(Account :: bitstring(), Platform :: bitstring(), ZoneId :: non_neg_integer(), Name :: auto | bitstring(), Sex :: non_neg_integer(), Classes :: non_neg_integer()) -> #role{}.
create(Account, Platform, ZoneId, Name, Sex, Classes) ->
    %% 角色id 不赋值
    Id = 0,
    #role{
        id = {Id, Platform, ZoneId}
        , platform = Platform
        , zone_id = ZoneId
        , account = Account
        , name = Name
        , sex = Sex
        , classes = Classes
        , init_flag = true

        , assets = assets:init()
        , p_bag = package:init(bag)
        , p_store = package:init(store)
    }.


%% @doc 获取角色列表，为了支持混服，所以参数要求传入平台标识
-spec list(list(), list()) -> [list()] | {error, term()}.
list(Account, _Platform) when is_list(Account) ->
    Result = sql_conn_mgr:get_all("select id, platform, zone_id, name, sex, lev, classes from role where account = ? order by lev desc", [Account]),
    case Result of
        {ok, L} -> L;
        Err -> Err
    end.


%% @doc 保存角色数据
-spec save(Type :: cache | to_db, #role{}) -> ok | {error, term()}.
save(Type, Role = #role{}) ->
    role_data:local_save(Type, Role).


%% @doc 获取指定的角色数据(自动进行版本转换)
-spec local_fetch(by_id, pos_integer(), list(), non_neg_integer()) ->
    {ok, #role{}} | {error, term()};
    (by_acc, list(), list(), non_neg_integer()) ->
    {ok, #role{}} | {error, term()};
    (by_name, list(), list(), non_neg_integer()) ->
    {ok, #role{}} | {error, term()}.
local_fetch(by_name, Name, Platform, ZoneId) when is_list(Name) ->
    case sql_conn_mgr:get_one("select id from role where name = ? and platform = ? and zone_id = ?", [Name, Platform, ZoneId]) of
        {ok, undefined} -> {error, role_data_not_found};
        {ok, Rid} -> local_fetch(by_id, Rid, Platform, ZoneId);
        {error, Error} ->
            ?ERR("执行数据库查询发生异常: ~w", [Error]),
            {error, error}
    end;
local_fetch(by_id, Rid, Platform, ZoneId) when is_integer(Rid) ->
    %% 先从磁盘缓存中查询，找不到时再从数据库中查询
    case dets:lookup(?RID_TO_TAB(Rid), {Rid, Platform, ZoneId}) of
        %% 此处不能直接把查询到的角色数据当成#role{}，因为有可能版本不一致
        [Term] when is_tuple(Term) ->
            role_ver:do(element(2, Term), Term);
        [] ->
            case sql_conn_mgr:get_row("select ver, lock_status, name, data from role where id = ? and platform = ? and zone_id = ?", [Rid, Platform, ZoneId]) of
                {ok, undefined} -> {error, role_data_not_found};
                {ok, [Ver, _LockStatus, _Name, Data]} ->
                    case util:string_to_term(Data) of
                        {error, Why} ->
                            ?ERR("~w的扩展数据无法正确转换成term(): ~w", [{Rid, Platform, ZoneId}, Why]),
                            {error, fetch_failure};
                        {ok, Term} ->
                            role_ver:do(Ver, Term)
                    end;
                {error, Err} -> {error, Err}
            end;
        Err ->
            ?ERR("角色数据磁盘缓存异常: ~w", [Err]),
            {error, role_cache_exception}
    end.

local_fetch_role_id(by_name, Name, Platform, ZoneId) when is_list(Name) ->
    case sql_conn_mgr:get_one("select id from role where name = ? and platform = ? and zone_id = ?", [Name, Platform, ZoneId]) of
        {ok, undefined} -> {error, role_data_not_found};
        {ok, Rid} -> {ok, {Rid, Platform, ZoneId}};
        {error, Error} ->
            ?ERR("执行数据库查询发生异常: ~w", [Error]),
            {error, error}
    end.

%% @doc 获取角色列表(先从数据库取出来,再从缓存中读,如果缓存有的话,以缓存数据为准)
-spec get_role_list(by_acc, list(), list(), non_neg_integer()) -> {ok, RoleList :: list()} | {error, term()}.
get_role_list(by_acc, Acc, Platform, ZoneId) ->
    case sql_conn_mgr:get_all("select id, platform, zone_id, name, sex, lev, classes from role where account = ? and platform = ? and zone_id = ?", [Acc, Platform, ZoneId]) of
        {ok, []} -> {ok, []};
        {ok, List} ->
            RoleList = get_role_list(by_id_list, List),
            {ok, RoleList};
        {error, Error} ->
            ?ERR("执行数据库查询发生异常: ~w", [Error]),
            {error, error}
    end.
get_role_list(by_id_list, RoleList) when is_list(RoleList) ->
    NRoleList = get_role_list_f1(RoleList, []),%% 查询是否有在缓存中,如果缓存有数据的话,以缓存数据为准
    NRoleList.
get_role_list_f1([], RoleInfoList) -> lists:reverse(RoleInfoList);
get_role_list_f1([[Rid, Platform, ZoneId, Name, Sex, Lev, Class] | T], RoleInfoList) ->
    NRoleInfoList =
        case local_fetch_from_dets(by_id, Rid, Platform, ZoneId) of%% 缓存有数据,以缓存数据为准
            {ok, #role{name = Name1, sex = Sex1, lev = Lev1, classes = Class1}} ->
                [[Rid, Platform, ZoneId, Name1, Sex1, Lev1, Class1] | RoleInfoList];
            {ok, no_cache} -> [[Rid, Platform, ZoneId, Name, Sex, Lev, Class] | RoleInfoList];
            _ -> RoleInfoList
        end,
    get_role_list_f1(T, NRoleInfoList).

local_fetch_from_dets(by_id, Rid, Platform, ZoneId) when is_integer(Rid) ->
    case dets:lookup(?RID_TO_TAB(Rid), {Rid, Platform, ZoneId}) of
        %% 此处不能直接把查询到的角色数据当成#role{}，因为有可能版本不一致
        [Term] when is_tuple(Term) ->
            role_ver:do(element(2, Term), Term);
        [] -> {ok, no_cache};
        Err ->
            ?ERR("角色数据磁盘缓存异常: ~w", [Err]),
            {error, role_cache_exception}
    end.

%% @doc 保存角色数据，注意:to_db的调用频度不可太高
-spec local_save(cache | to_db, #role{}) -> ok | {error, term()}.
local_save(_Type, #role{sex = ?sex_unknown}) -> %% 性别没选就不保存了
    ok;
local_save(_Type, #role{is_robot = true}) -> %% 机器人不保存
    ok;
local_save(Type, Role = #role{id = {Rid, _P, _Z}}) ->
    %% @todo 把一些需要在写入前进行处理的数据加入下面
    R = Role#role{
        trigger = {1, []}           %% 清空trigger
        , link = ?undefined
        , pid = ?undefined
        , timer = ?undefined
    },
    dets:insert(?RID_TO_TAB(Rid), R),
    case Type of
        to_db ->
            save_to_db(R);
        _ -> ok
    end.

%% @doc 创建保存角色,用insert判定插入是否重名
create_save(Role = #role{id = {_, P, Z}}) ->
    R = Role#role{},
    %% TODO 这里是否要采用此类
    case create_save_to_db(R) of
        {ok, Rid} ->
            dets:insert(?RID_TO_TAB(Rid), R#role{id = {Rid, P, Z}}),
            {ok, Rid};
        {error, _} ->
            {false, ?T("这个名字太普通，已经有人使用了")}
    end.

%% @doc 清除指定角色数据的磁盘缓存
clean_cache({Rid, _P, _Z} = RoleId) ->
    dets:delete(?RID_TO_TAB(Rid), RoleId).


%% ----------------------------------------------------
%% 内部处理
%% ----------------------------------------------------

init([]) ->
    ?INFO("[~w] 正在启动...", [?MODULE]),
    process_flag(trap_exit, true),
    NextId =
        case sql_conn_mgr:get_one("select max(id) from role") of
            {ok, MaxId} when is_integer(MaxId) -> MaxId + 1;
            _ -> 1
        end,
    open_dets(?DETS_NUM),
    sync_to_db(),
    %% 账号检查表
    ets:new(ets_role_acc_data, [set, named_table, public, {keypos, 1}]),
    State = #state{next_id = NextId},
    ?INFO("[~w] 启动完成", [?MODULE]),
    {ok, State}.

%% 获取一个角色ID
handle_call(fetch_role_id, _From, State = #state{next_id = Id}) ->
    {reply, {ok, Id}, State#state{next_id = Id + 1}};


handle_call(_Request, _From, State) ->
    {noreply, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info({set_next_id, Id}, State) ->
    {noreply, State#state{next_id = Id + 1}};

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ?INFO("[~w] 正在关闭...", [?MODULE]),
    ?INFO("[~w] 关闭完成", [?MODULE]),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% ----------------------------------------------------
%% 私有函数
%% ----------------------------------------------------
%% 启动日志处理进程
open_dets(0) -> ok;
open_dets(N) ->
    dets:open_file(?ROLE_DATA_TAB(N), [{file, lists:concat(["./dets/role_", N, ".dets"])}, {keypos, #role.id}, {type, set}]),
    open_dets(N - 1).


%% @doc 同步磁盘缓存中的数据到数据库中，在开机时处理
sync_to_db() -> sync_to_db(?DETS_NUM).
sync_to_db(0) -> ?INFO("所有DETS中的所有角色数据已经执行同步操作");
sync_to_db(N) ->
    Name = ?ROLE_DATA_TAB(N),
    case dets:first(Name) of
        '$end_of_table' ->
            ?INFO("[~ts]中没有角色数据需要同步到数据库", [Name]);
        _ ->
            dets:traverse(Name,
                fun(Term) ->
                    case role_ver:do(element(2, Term), Term) of
                        {error, Err} ->
                            ?ERR("[~ts]同步磁盘缓存数据到数据库时发生异常: ~w", [Name, Err]);
                        {ok, Role} ->
                            case save_to_db(Role) of
                                ok ->
                                    clean_cache(Role#role.id);
                                {error, Err} ->
                                    ?ERR("[~ts]中角色数据[~ts]同步到数据库时发生异常: ~w", [Name, Role#role.name, Err])
                            end
                    end,
                    continue
                end
            ),
            ?INFO("[~ts]中所有角色数据已经执行同步操作", [Name])
    end,
    sync_to_db(N - 1).

%% @doc 存储角色完整数据到数据库中
save_to_db(R = #role{id = {Rid, P, Z}, ver = Ver, account = Acc, channel_reg = ChannelReg, lock_status = LockStatus, name = Name, sex = Sex, classes = Classes, lev = Lev}) ->
    case sql_conn_mgr:exec("replace into role (id, platform, zone_id, account, channel_reg, name, lock_status, sex, classes, lev, ver, data) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [Rid, P, Z, Acc, ChannelReg, Name, LockStatus, Sex, Classes, Lev, Ver, util:term_to_string(R)]) of
        ok -> ok;
        {error, Err} ->
            ?ERR("[~ts]的角色数据存入数据库时发生异常: ~w", [Name, Err]),
            {error, Err}
    end.

%% @doc 创建角色存储, 用insert是避免主键重复,确保新增成功
create_save_to_db(R = #role{id = {_Rid, P, Z}, ver = Ver, account = Acc, channel_reg = ChannelReg, lock_status = LockStatus, name = Name, sex = Sex, classes = Classes, lev = Lev}) ->
    case sql_conn_mgr:insert("insert into role (platform, zone_id, account, channel_reg, name, lock_status, sex, classes, lev, ver, data) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [P, Z, Acc, ChannelReg, Name, LockStatus, Sex, Classes, Lev, Ver, util:term_to_string(R)]) of
        {ok, NewId} ->
            {ok, NewId};
        {error, Err} ->
            ?ERR("[~ts]的角色数据存入数据库时发生异常: ~w", [Name, Err]),
            {error, Err}
    end.

%% @doc 存储角色简单数据到数据库中(除了Role不存)
save_simple_to_db(#role{id = {Rid, P, Z}, name = Name, lev = Lev}) ->
    case sql_conn_mgr:exec("update role set lev = ? where id = ? and platform = ? and zone_id = ?", [Lev, Rid, P, Z]) of
        ok -> ok;
        {error, Err} ->
            ?ERR("[~ts]的角色简单数据存入数据库时发生异常: ~w", [Name, Err]),
            {error, Err}
    end.

%% ----------------------------------------------------
%% 内部
%% ----------------------------------------------------

%% 角色起名判定
create_name(Name, {_, _ConnPid}) ->
    %% 数据库判定重名
    case sql_conn_mgr:get_one("select count(*) from role where name = ?", [Name]) of
        {error, _Err} ->
            {false, ?T("系统繁忙，请稍后再试!")};
        {ok, Num} when Num >= 1 ->
            {false, ?T("角色名已被使用")};
        {ok, _} ->
            true
    end.


%% role结构检测，防止非法清空！
check_return(#role{init_flag = true}) -> true;
check_return(_) -> {error, bad_role}.
